// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_JS_FLOW_EXECUTOR_IMPL_H_
#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_JS_FLOW_EXECUTOR_IMPL_H_

#include <memory>
#include <string>
#include "base/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/values.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/devtools/devtools_client.h"
#include "components/autofill_assistant/browser/js_flow_executor.h"

namespace autofill_assistant {

// Executes a JS flow in a sandbox JS context. The flow may request additional
// native actions to be performed by its delegate.
class JsFlowExecutorImpl : public JsFlowExecutor {
 public:
  // |delegate| must outlive the JsFlowExecutorImpl.
  JsFlowExecutorImpl(content::WebContents* web_contents, Delegate* delegate);
  ~JsFlowExecutorImpl() override;
  JsFlowExecutorImpl(const JsFlowExecutorImpl&) = delete;
  JsFlowExecutorImpl& operator=(const JsFlowExecutorImpl&) = delete;

  // Starts executing |js_flow| in an isolated JS context. Once finished (or on
  // error), |result_callback| is invoked with the final result. In the case of
  // an uncaught exception during flow execution, the returned status may
  // contain a stack trace and additional information (limited to the sandbox).
  // Only one flow may run at a time.
  //
  // Flows may request additional native actions from the delegate, using the
  // following syntax:
  //
  // let [status, result] = await runNativeAction('request')
  //
  // - |status| is an int corresponding to a ProcessedActionStatusProto.
  // - [result] is a struct containing the result value, or an empty struct if
  //            no result was returned. The specific contents depend on the
  //            native action.
  // - |runNativeAction| is provided automatically and takes a single argument.
  //                     The type of the argument will depend on what the native
  //                     delegate expects, but will typically be a serialized
  //                     protobuffer.
  //
  // The flow result is one of the following, depending on the |js_flow|:
  // (1) ACTION_APPLIED and a base::Value dictionary containing the 'result' key
  // and value, as returned by the JS flow. Example for a one-liner js flow
  // 'return 12345':
  // {
  //   "result": {
  //     "description": "12345",
  //     "type": "number",
  //     "value": 12345
  //   }
  // }
  // See the unit tests for further examples. Note: field names are
  // auto-generated by base::Value serializers.
  //
  // (2) UNEXPECTED_JS_ERROR in case of an execution error, along with a
  // base::Value dictionary containing the 'exceptionDetails' key and exception,
  // if available. Note that this exception originates from the sandbox JS
  // context. Example:
  // JS flow:
  //   function doSomething(x) {
  //       console.log('foobar says: ' + x);
  //       throw new Error('Hello world!');
  //   }
  //   function entrypoint() {
  //       doSomething('bla');
  //   }
  //   entrypoint();
  //
  // Returned exception details:
  //   "exceptionDetails": {
  //      "columnNumber": 0,
  //      "exception": {
  //         "className": "Error",
  //         "description": "Error: Hello world!
  //                           at doSomething (<anonymous>:16:33)
  //                           at entrypoint (<anonymous>:19:27)
  //                           at <anonymous>:21:25
  //                           at <anonymous>:22:28",
  //         "objectId": "-3968045700143737919.4.2",
  //         "subtype": "error",
  //         "type": "object"
  //      },
  //      "exceptionId": 2,
  //      "lineNumber": 0,
  //      "text": "Uncaught (in promise) Error: Hello world!"
  //   }
  //
  // (3) UNEXPECTED_JS_ERROR and null in case of internal errors during script
  // execution (i.e., unrecoverable devtools errors). The status details may
  // contain additional information.
  void Start(const std::string& js_flow,
             base::OnceCallback<void(const ClientStatus&,
                                     std::unique_ptr<base::Value>)>
                 result_callback) override;

 private:
  void InternalStart();
  void OnGetFrameTree(const DevtoolsClient::ReplyStatus& reply_status,
                      std::unique_ptr<page::GetFrameTreeResult> result);
  void IsolatedWorldCreated(
      const DevtoolsClient::ReplyStatus& reply_status,
      std::unique_ptr<page::CreateIsolatedWorldResult> result);
  void RefreshNativeActionPromise();
  void OnNativeActionRequested(const DevtoolsClient::ReplyStatus& reply_status,
                               std::unique_ptr<runtime::EvaluateResult> result);
  void OnNativeActionRequestActionRetrieved(
      const std::string& js_array_object_id,
      const DevtoolsClient::ReplyStatus& reply_status,
      std::unique_ptr<runtime::CallFunctionOnResult> result);
  void OnNativeActionRequestFulfillPromiseRetrieved(
      std::unique_ptr<base::Value> action_request,
      const DevtoolsClient::ReplyStatus& reply_status,
      std::unique_ptr<runtime::CallFunctionOnResult> result);
  void OnNativeActionFinished(const std::string& fulfill_promise_object_id,
                              const ClientStatus& status,
                              std::unique_ptr<base::Value> result);
  void OnFlowResumed(const DevtoolsClient::ReplyStatus& reply_status,
                     std::unique_ptr<runtime::CallFunctionOnResult> result);
  void OnFlowFinished(const DevtoolsClient::ReplyStatus& reply_status,
                      std::unique_ptr<runtime::EvaluateResult> result);
  void RunCallback(const ClientStatus& status,
                   std::unique_ptr<base::Value> result_value);

  // Returns true if |reply_status| and |result| are ok. Else, stops the flow
  // and returns false.
  template <typename T>
  bool CheckResultAndStopOnError(
      const DevtoolsClient::ReplyStatus& reply_status,
      std::unique_ptr<T>& result,
      const char* file,
      int line) {
    ClientStatus status =
        CheckJavaScriptResult(reply_status, result.get(), file, line);
    if (!status.ok()) {
      RunCallback(status, (result != nullptr ? result->Serialize() : nullptr));
      return false;
    }
    return true;
  }

  Delegate* const delegate_;
  std::unique_ptr<DevtoolsClient> devtools_client_;
  int isolated_world_context_id_ = -1;

  // Only set during a flow.
  std::unique_ptr<std::string> js_flow_;
  base::OnceCallback<void(const ClientStatus&, std::unique_ptr<base::Value>)>
      callback_;

  base::WeakPtrFactory<JsFlowExecutorImpl> weak_ptr_factory_{this};
};

}  // namespace autofill_assistant

#endif  // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_JS_FLOW_EXECUTOR_IMPL_H_
